一个跟状态位处理有关的应用案例
有STM32用户使用STM32H7xx芯片进行开发,用到SPI外设。通过定时器定时触发SPI的发送,但发现每次发送的数据跟本来设计的不一样。本来他是希望每次发生定时器更新事件,在更新中断里发送一个16位数据,可他发现发送的数据却是32位,通过SPI的时钟信号可以清楚的看到每次发送的32个时钟脉冲。这是怎么回事呢?
经过反复确认,跟SPI有关的配置方面没有发现任何问题。
这里我就基于上述问题,稍加拓展地做些验证分析。我找了块STM32H743的Nucleo板进行些验证测试。使用SPI1,让其工作在双工主模式,MOSI与MISO短接,自发自收。
首先,我用DMA的方式来传输数据。使用TIM3的更新事件触发DMA,通过DMA将内存数据写到SPI1的发送数据寄存器,同时SPI1的接收也开启DMA传输,即通过SPI1的接收事件触发DMA传输,将SPI1收到的数据搬到内存数组。
使用STM32CubeMx工具进行初始化配置。
1、对TIM3进行基本配置,并使能更新事件的DMA请求。【这里将DMA配置为循环模式以便测试】
2、对SPI1进行初始化配置。工作在双工主模式,数据宽度选择16位,开启SPI接收事件的DMA传输。
做些其它有关调试、时钟的配置后,生成初始化代码。
准备需要的数据变量并添加相关用户代码。
编译无误后,运行看结果。很遗憾,结果不妙!
SPI1根本没收到数据,也就是说数据也没发出去。怎么回事呢?难道是DMA访问不到相应的外设或内存?借助调试环境可以看到,目前用到的内存属于片内AXI SRAM.
SPI1属于D2域,与APB2总线相连。
我们通过查看STM32H7参考手册中关于主从设备互联矩阵图可以看出,不论AXI SRAM还是SPI1,DMA1都是可以访问到的。
另外,在调试界面,可以发现定时器触发的DMA已经在工作了,该DMA通道的数据寄存器在不停变化。看来很可能是SPI1本身还没工作。
继续核对SPI有关的配置和用户代码。在查看库代码里有关SPI的DMA发送函数HAL_SPI_Transmit_DMA()的具体内容时,发现了一行特别的代码。【注:不是所有STM32系列的SPI都有这个控制位】。
难道是缺少这句代码?
我将SET_BIT(SPI1->CR1, SPI_CR1_CSTART);加到我的用户代码里面,并放在使能SPI1外设代码的前面。
编译,运行,很遗憾,还是没动静!再去看看手册关于这个CSTART位有什么说法。我们从STM32H7的手册里可以看到相关描述。
该位是作为工作在主模式的SPI的传输启动控制位,对其置1启动传输,而且对其置1必须在SPE=1并工作在主模式的前提下。看来,刚才对其置位的代码放的位置还有问题,应该放在__HAL_SPI_ENABLE(&hspi1);代码的后面。
我将用户代码调整如下:
再做编译、运行,收发正常了。
通过示波器观察,定时器定时触发,每触发1次,SPI收发1个16位数据,从时钟信号来看,很标准,没有发现问题。
不过,人家反馈的是在定时器中断里发送数据遇到的问题,那就试试中断发送数据的情况吧。我们将代码稍作修改,使能定时器更新中断,屏蔽掉定时器事件触发DMA的相关代码,保留SPI1的DMA接收配置代码,然后在更新中断里给SPI1的发送数据寄存器写数据。
我将TIM3更新中断服务代码写得非常简单,总共就两行代码【主要为了模拟客户的用法,因为客户的中断服务代码也非常简单】:
每进一次定时器更新中断,先清中断标志,然后发送一个16位数据。
编译运行,查看接收的数据,并观察每次发送数据的时钟情况,也没有发现异常。每个数据严格地对应着16个时钟脉冲。
后来经过进一步了解。客户只有在将中断服务程序放在RAM里运行时才会出现每发送1个16位数据而出现32个时钟脉冲的奇怪情况!换句话说,如果他的中断服务程序在Flash里跑的时候是不会出现异常的。
既然这样,看来我的代码还需要稍微调整下,目前我的代码都是运行在FLASH里的。现在将定时器中断服务程序放到RAM里面去运行。我不妨将中断服务程序放到DTCM里去运行,这样可以保证代码执行得足够快。
经查手册可以得知DTCM的地址范围是20000000~2001ffff.
我这里使用的ARM MDK开发环境,稍微配置下,让中断服务程序在DTCM里运行。
编译运行,查看结果。还真的出现了每进一次中断,发送的数据不是16位而是32位了,从时钟信号线上可以清晰地看到。输出波形变成下面的样子:
可我在中断里的确每次只发送1个16位数据怎么变成32位了呢?结合波形图,明显是连续发了两遍数据。
不太可能是因为定时周期太短的原因,因为我再怎么加长定时器的溢出周期,这个问题还是存在。这很容易让人想到是不是程序连续进了2次中断。但中断服务代码里已经做了更新中断标志的清除动作了,而且也没有开启其它定时器中断。难道更新中断服务程序刚执行完后又返回重进了一次了?!
如果说那个更新中断标志不能被清除的话,也不像。因为若不能被清除,那应该是没完没了的进中断,就不是才进两次的问题了。难道是中断请求标志位第一次没有被清除,第二次才被清除?
尝试在那句赋值语句后面稍微加一串NOP延时,异常消失了!即恢复成每进一次中断发一个16位数据。那为什么要一点点延时呢?
。。。。。。
其实,因为每次进入更新中断服务程序时,CPU先执行清除更新中断请求标志的程序代码,然后执行给SPI数据发送寄存器赋值的代码。执行完中断程序退出之后发现那个更新中断请求标志还没有完成清零,就再次进入了中断服务程序,又将刚才的服务代码跑了一遍。第二次退出之后,更新中断标志的清零状态也实现了。显然,在第2次运行中断服务程序时将SPI的数据寄存器又做了一次赋值,就导致了每触发一次更新中断,连续运行了2次中断服务程序而发送2次SPI数据。
那么,这里就会产生一连串的疑问:
1、 为什么会出现更新中断标志不能一次清零而需要做两次清除操作呢?
首先,更新中断请求标志不是不能实现一次性被清零,而是CPU执行完清除更新中断标志的指令代码后,该标志位完成清零状态需要一定的时间。但在这个时间段,CPU还是流水般不停往下执行程序代码。由于这里代码量少、程序执行速度快,当中断服务程序代码执行完以后,那个标志的清零状态还没出现,无奈再进了一次中断。
其次,至于那个2次清除操作是个误会。客观上因为连续进了2次中断而运行了两次清零操作代码,但这不是必须的,只是上次清零操作的效果还在路上。即使第2次进中断时不执行清零操作代码,它依然会实现清零状态,只是时间点没到。
2、 为什么中断程序代码在FLASH里运行没问题而在RAM里运行就有问题呢?
这是因为代码在RAM里运行的速率要快过在FLASH里运行,以至于在FLASH运行完上述中断服务程序后那个更新中断标志就完成了清零状态的改变,所以不会导致中断服务程序再跑一次。而当代码在FLASH里运行或者说在其它较为低速的MCU运行时是没有机会碰到这种状况的。其实,即使在STM32H7XX芯片的RAM里运行,当运行完清零动作的程序代码后若还要运行比上面测试代码稍微复杂点的程序代码时也完全可能碰不到现有的情形。
3、 如何改善或解决这种情况呢?
其实,这个情况并非什么新情况,实质上它就是个状态位的管理问题。我们知道,指令代码的执行时间,可能跟执行指令代码后相应的结果或状态的响应时间并不一致。
我们不妨打个生活中的例子。比方你对操场对面的伙伴发口令“立刻过来一起去办事”,口令可以发得很快,但对方接收指令并完成动作的速率往往没法与你发令者同步。当对方收到口令并全速向你跑来的过程中,你可能有多种行为选择。你可能先去忙点别的,忙完之后看他还没过来时就再发口令催促。只是这个催促指令对于对方跑过来的结果或效果是没有意义的,因为不论你催不催,他跑过来也要那么多时间,你只是重复了指令;或者你静静的等着他跑过来,等他过来后再一起去办事情;也有可能你发完口令后就自顾自地去办理本需要他协助才能完成的事情等等。
在MCU应用中也有类似的情形存在。当后序的程序代码执行了,而前序程序代码的执行结果或状态还没有出现是完全可能的,尤其加上流水线指令执行架构更加速了后序代码的执行。
像遇到这种前序指令代码执行后的结果或状态会影响到后序指令代码的正常运行的情形时,我们常规的通常做法就是,在执行相应指令代码后,轮询对应状态位是否生效,这个过程往往会伴随一定的延时等待,但这个操作很多时候必须的。否则后序指令代码的运行可能是盲目、无效或非法等。这类操作在MCU应用中其实很多。比方运行时钟配置代码时,往往需轮询相应标志位才决定后续代码的走向;比方在FLASH的改写操作里,往往需要轮询上一个操作是否完成来决定后续代码的运行;再比方做RTC配置时,执行完写允许、初始化使能的相应代码后,需查询相应标志位生效后才继续后续操作等等。【当然,如果基于ST官方库的话,很多代码细节可能已经封装在库函数里了。】
具体到这里,问题实质跟上面提到的是一样的,可能给人感觉上稍显有点新鲜。毕竟在以往CPU主频较低时几乎没有机会遇到这种情况的,通常针对相关中断请求标志位做个清零操作就完事了,退出中断服务程序之后一般不用担心它还未完成清零效果。【当然,前面也说了即使使用STM32H7也并不容易碰到。只有当中断代码在RAM里运行且中断服务程序非常精简的前提下才会遇到。】
鉴于现有中断服务代码的运行情况,为了避免中断标志位因退出中断时还未完成清零而再次进中断,进而导致些误操作,我们只需将代码稍加修改,增加了一行有关定时器更新中断请求标志是否被清零的状态轮询语句。
修改之后,经过反复测试验证都一切正常。
好,不知不觉啰嗦不少了,就此打住。祝君好运!祝君健康~
======================